package ch.bfh.java.directorysynchronizer.business;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JFrame;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import ch.bfh.java.directorysynchronizer.presentation.Frame.FileDiffDialog;
import ch.bfh.java.directorysynchronizer.presentation.Frame.FileDiffDialog.Response;

/**
 * @author Matthias Winzeler <winzm2@bfh.ch>
 * @author Christian Mueller <muellc9@bfh.ch>
 *
 */
public class DirectorySynchronizer
{
	static Logger logger = Logger.getLogger(ch.bfh.java.directorysynchronizer.business.DirectorySynchronizer.class);
	
	private File sourceDir;
	private File targetDir;
	private JFrame parent;
		
	private boolean isStopped = false;
	
	private boolean remember = false;
	private Response rememberedResponse;
	
	public DirectorySynchronizer(JFrame parent, File sourceDir, File targetDir)
	{		
		this.parent = parent;
		this.sourceDir = sourceDir;
		this.targetDir = targetDir;
		logger.setLevel(Level.INFO);
	}
	
	public void start()
	{

		logger.info("Starting synchronization");
		synchronizeDir(this.sourceDir, this.targetDir);
		logger.info("Finished synchronization");

		
	}
	
	private void synchronizeDir(File sourceDir, File targetDir)
	{
		logger.info("Synchronizing directory " + sourceDir + " to " + targetDir);

		logger.debug("Checking source dir " + sourceDir);
		
		if(sourceDir == null || !sourceDir.isDirectory())
		{
			throw new DirectorySynchronizeException("Source directory " + sourceDir + " is not a directory");
		}
		
		if(!(sourceDir.canExecute() || sourceDir.canRead()))
		{
			throw new DirectorySynchronizeException("Source directory " + sourceDir + " must be readable and executable");
		}
		
		logger.debug("Checking target dir " + targetDir);
		
		if(targetDir == null || !targetDir.isDirectory())
		{
			throw new DirectorySynchronizeException("Target directory " + targetDir + " is not a directory");
		}
		
		if(!(targetDir.canWrite() || targetDir.canExecute()))
		{
			throw new DirectorySynchronizeException("Target directory " + targetDir + " must be writable and executable");
		}
		
		logger.debug("Listing files of source directory");
		
		List<String> sourceFiles = new ArrayList<String>();
		List<String> sourceDirs = new ArrayList<String>();
		
		for(File currentSource : sourceDir.listFiles())
		{
			if(isStopped)
			{
				logger.info("Synchronization stopped.");
				break;
			}
			
			logger.debug("Synchronizing source " + currentSource);
			
			File currentTarget = new File(targetDir.getAbsolutePath() + File.separator + currentSource.getName());
			logger.debug("Calculated target: " + currentTarget);
					
			if(currentSource.isDirectory())
			{
				sourceDirs.add(currentSource.getName());
				
				logger.debug("Source is a directory, invoking recursively");
				
				if(!currentTarget.exists())
				{
					logger.info("Target " + currentTarget + " does not exist, creating...");
					currentTarget.mkdir();
				}
				
				if(currentTarget.isDirectory())
				{
					synchronizeDir(currentSource, currentTarget);
				}
				else if(currentTarget.isFile())
				{
					logger.error("Source is a directory, but target " + currentTarget + " is a file. Skipping...");
				}
				else
				{
					logger.error("Source is a directory, but target " + currentTarget + " is something else. Skipping...");
				}
			}
			else if(currentSource.isFile())
			{
				sourceFiles.add(currentSource.getName());
				logger.debug("Source is a file, synchronizing...");
				synchronizeFile(currentSource, currentTarget);
			}
			else
			{
				//TODO symlink or something else, ignoring
				logger.warn("Source " + currentSource + " is not a directory or a file, do not know how to synchronize, skipping...");
			}
		}
		
		logger.debug("Removing all contents of target that are not in source");
		
		if(!isStopped)
		{
			deepDeleteDir(targetDir, sourceDirs, sourceFiles);
		}
	}
	
	private void deepDeleteDir(File dir, List<String> directoriesToKeep, List<String> filesToKeep)
	{
		for(File item : dir.listFiles())
		{
			if(item.isDirectory())
			{
				if(!directoriesToKeep.contains(item.getName()))
				{
					deepDeleteDir(item, Collections.<String>emptyList(), Collections.<String>emptyList());
				}
			}
			else if(item.isFile())
			{
				if(!filesToKeep.contains(item.getName()))
				{
					if(item.canWrite())
					{
						logger.info("Deleting obsolete file " + item);
						item.delete();
					}
					else
					{
						logger.error("Could not delete obsolote target " + item + " because its not writable");
					}
				}
			}
		}
		
		logger.info("Deleting obsolete dir " + dir);
		dir.delete();
	}
	
	private void synchronizeFile(File source, File target)
	{		
		logger.debug("Synchronizing file " + source + " to " + target);
		if(!source.canRead())
		{
			throw new DirectorySynchronizeException("File to be synced " + source + " can not be read.");
		}
		
		if(!target.exists())
		{
			logger.info("Target file " + target + " does not exist, copying source");
			try
            {
	            copyFile(source, target);
            }
            catch(IOException e)
            {
	            logger.error("Could not copy file " + source + " to " + target + ": " + e.getLocalizedMessage());
            }
		}
		else
		{
			if(target.isFile())
			{
				logger.info("Target file " + target + " is a file that exists, comparing...");
				
				//file exists, compare these two
				if(areFilesDifferent(source, target))
				{
					Response response = null;
					logger.info("Target and source file are different, asking for user decision");
					
					if(!remember)
					{						
						FileDiffDialog fileDiffDialog = new FileDiffDialog(parent, source, target);
						fileDiffDialog.setVisible(true);
						
						response = fileDiffDialog.getResponse();

						if(response == Response.CANCEL)
						{
							stop();
						}
						
						if(fileDiffDialog.getRemember())
						{
							logger.info("Saved decision " + response + " for next time");
							remember = true;
							rememberedResponse = response;
						}
					}
					else
					{
						response = rememberedResponse;
						logger.info("Applying remembered decision " + response);
					}
					
					if(response == Response.OVERWRITE )
					{
						try
                        {
	                        copyFile(source, target);
                        }
                        catch(IOException e)
                        {
                        	logger.error("Could not overwrite file " + source + " to " + target + ": " + e.getLocalizedMessage());
                        }
					}
				}
				else
				{
					logger.info("Target file " + target + " is already synchronized, skipping");
				}
			}
			else
			{
				//TODO source is file, target is something else, skipping..
				logger.error("Target " + target + " must be a file but is not, skipping...");
			}
		}
	}
	
	private boolean areFilesDifferent(File file1, File file2)
	{
		String file1Hash = getMD5Hash(file1);
		String file2Hash = getMD5Hash(file2);
		
		logger.debug("MD5 of " + file1 + " is " + file1Hash);
		logger.debug("MD5 of " + file2 + " is " + file2Hash);
		
		return !file1Hash.equals(file2Hash);
	}
	
	/**
	 * Got code snippet from http://www.rgagnon.com/javadetails/java-0416.html
	 */
	private String getMD5Hash(File file)
	{
		InputStream fis;
		try
		{
			fis = new FileInputStream(file);
		}
		catch(FileNotFoundException e)
		{
			throw new DirectorySynchronizeException("Could not find file "
			        + file + "; was it deleted in the meantime?");
		}

		byte[] buffer = new byte[1024];
		MessageDigest complete;
		try
		{
			complete = MessageDigest.getInstance("MD5");
		}
		catch(NoSuchAlgorithmException e)
		{
			throw new DirectorySynchronizeException(
			        "Your version of java does not support MD5. Please update.");
		}
		try
		{
			int numRead;
			do
			{
				numRead = fis.read(buffer);
				if(numRead > 0)
				{
					complete.update(buffer, 0, numRead);
				}
			} while(numRead != -1);
			fis.close();
		}
		catch(IOException e)
		{
			throw new DirectorySynchronizeException(
			        "Error while calculating MD5 hash of file " + file);
		}
		byte[] b = complete.digest();
		String result = "";

		for(int i = 0; i < b.length; i++)
		{
			result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
		}
		
		return result;
	}
	
	/**
	 * Got code snippet from http://stackoverflow.com/questions/106770/standard-concise-way-to-copy-a-file-in-java 
	 */
	private void copyFile(File sourceFile, File destFile) throws IOException
	{
		if(isStopped)
		{
			return;
		}
		
		if(!destFile.exists())
		{
			destFile.createNewFile();
		}

		FileChannel source = null;
		FileChannel destination = null;
		try
		{
			source = new FileInputStream(sourceFile).getChannel();
			destination = new FileOutputStream(destFile).getChannel();
			destination.transferFrom(source, 0, source.size());
		}
		finally
		{
			if(source != null)
			{
				source.close();
			}
			if(destination != null)
			{
				destination.close();
			}
		}
	}
	
	public void stop()
	{
		logger.error("Directory synchronizing forcefully stopped!");
		isStopped = true;
	}
}
